42 数据结构之列表
42.1 引言列表在数据管理中的核心地位
理论背景:动态数组的实现原理
Python列表(List)是基于动态数组(Dynamic Array)实现的顺序数据结构。从计算机科学的角度来看,列表的设计体现了空间与时间的精妙平衡:
- 连续内存存储: 列表元素在内存中连续存储,利用局部性原理(Locality Principle),提高CPU缓存命中率
- 自动扩容机制: 当空间不足时,列表会自动分配更大的内存块并复制元素,这种策略被称为超额分配(Over-allocation)
- amortized O(1): 虽然扩容操作本身是O(n),但由于扩容频率低,分摊到每次操作的平均时间复杂度为O(1)
数学分析:扩容策略的时间复杂度证明
Python列表的扩容采用几何增长策略。假设列表当前容量为N,当需要扩容时,新容量约为:
\[ N_{new} \approx N_{old} \times 1.125 + C \]
其中C是一个常数。这种策略确保了: - 扩容次数: 对于插入n个元素,最多需要\(O(\log n)\)次扩容 - 总复制成本: \(n + n/1.125 + n/1.125^2 + \cdots \approx n \times \frac{1}{1-1/1.125} \approx 9n\) - amortized cost: \(\frac{9n}{n} = O(1)\)
列表的四大核心特性:
| 特性 | 技术实现 | 时间复杂度 | 金融应用场景 |
|---|---|---|---|
| 可变性 | 元素可直接修改 | O(1) | 实时价格更新 |
| 有序性 | 维护插入顺序 | - | 时间序列数据 |
| 异构性 | 存储任意类型对象 | - | 混合数据记录 |
| 动态性 | 自动扩容 | amortized O(1) | 增量数据采集 |
补充说明:为什么Python列表扩容系数是1.125?
这个数值是经过精密计算的结果。较小的系数(如1.5)会浪费更多内存,较大的系数(如2.0)会增加扩容频率。Python的1.125(约9/8)在内存使用和性能之间达到了最优平衡。相比之下,Java ArrayList使用1.5,C++ std::vector通常使用2.0。
42.2 列表的基本操作
平台任务解答代码
以下代码与教学平台任务要求完全一致:
# 注:该代码块包含未完成的填空代码,需要在平台上完成
# 注意:此答案不全,请查阅平台。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
name_index = ["道琼斯工业平均指数","富时100指数","标普500指数","恒生指数","日经225指数","上证指数","深证指数"] #创建指数名称列的列表
price_index = [41563.08,8376.63,5648.40,17989.07,38647.75,2842.21,8348.48] # 定义列表price_index
print(name_index[2]) #访问 "标普500指数" 这个元素
print(price_index.index(8376.63)) #找出 8376.63 这个元素所在的索引值
#任务二
name_index = ["道琼斯工业平均指数","富时100指数","标普500指数","恒生指数","日经225指数","上证指数","深证指数"]
price_index = [41563.08,8376.63,5648.40,17989.07,38647.75,2842.21,8348.48] # 定义列表price_index
name_index.append("法国CAC40指数") #按要求添加新元素
name_index.append("德国DAX指数") # 将股指名称添加到列表
name_index.append("新加坡海峡指数") # 将股指名称添加到列表
name_index.append("台湾加权指数") # 将股指名称添加到列表
print(name_index) #打印添加新元素后的name_index列表
price_index.append(7630.95) #按要求添加新元素
price_index.append(18906.92) # 将股指收盘点数添加到列表
price_index.append(3442.93) # 将股指收盘点数添加到列表
price_index.append(22268.09) # 将股指收盘点数添加到列表
print(price_index) #打印添加新元素后的price_index列表
#任务三
name_index = ["道琼斯工业平均指数","富时100指数","标普500指数","恒生指数","日经225指数","上证指数","深证指数"]
price_index = [41563.08,8376.63,5648.40,17989.07,38647.75,2842.21,8348.48] # 定义列表price_index
price_index.remove(38647.75) #删除"日经225指数"这个元素
price_index.insert(6,2674.45) #添加索引为6的"韩国综合指数"这个元素
print(name_index) # 输出指数数据
print(price_index) # 输出价格数据
#任务四
name_index = ["道琼斯工业平均指数","富时100指数","标普500指数","恒生指数","日经225指数","上证指数","深证指数"]
price_index = [41563.08,8376.63,5648.40,17989.07,38647.75,2842.21,8348.48] # 定义列表price_index
price_index.sort() #将price_index列表元素由小到大排序
print(price_index) # 输出价格数据
price_index.reverse() #将price_index列表元素翻转
print(price_index) # 输出价格数据
price_index.clear() #删除price_index列表全部元素(使用clear函数)
print(price_index) # 输出价格数据# 创建列表:存储券商股票名称
# 方括号[]是列表的字面量语法
# 列表可以包含任意类型的对象,这里存储字符串类型的股票名称
stocks = ['中信证券', '国泰君安', '海通证券', '华泰证券']
# 访问元素:索引从0开始
# stocks[0]访问第一个元素,索引0指向列表的第一个位置
print('第一个:', stocks[0]) # 输出: 中信证券
# 负数索引:-1表示最后一个元素
# 这种语法糖简化了对列表末尾元素的访问
print('最后一个:', stocks[-1]) # 输出: 华泰证券
# 切片操作:[start:end],包含start,不包含end
# stocks[:3]等价于stocks[0:3],获取前3个元素(索引0,1,2)
print('前3个:', stocks[:3]) # 输出: ['中信证券', '国泰君安', '海通证券']
# stocks[1:4]获取索引1到3的元素(不包含索引4)
# 切片操作创建列表的浅拷贝,不会修改原列表
print('第2-4个:', stocks[1:4]) # 输出: ['国泰君安', '海通证券', '华泰证券']
# 修改元素:通过索引直接赋值
# 列表的可变性允许我们原地修改元素,而不需要创建新列表
stocks[1] = '申万宏源' # 将索引1的元素从'国泰君安'改为'申万宏源'
print('\n修改后:', stocks) # 输出修改后的完整列表
# 添加元素:append()方法在列表末尾添加元素
# append()的时间复杂度是amortized O(1),非常高效
stocks.append('招商证券') # 在末尾添加'招商证券'
print('追加后:', stocks) # 输出: ['中信证券', '申万宏源', '海通证券', '华泰证券', '招商证券']
# 删除元素:remove()方法删除第一个匹配的元素
# remove()需要先查找元素,时间复杂度为O(n)
stocks.remove('海通证券') # 删除'海通证券'
print('删除后:', stocks) # 输出: ['中信证券', '申万宏源', '华泰证券', '招商证券']代码深度解析:
索引机制的内存模型:
- Python列表存储的是对象的引用,而非对象本身
- 每个引用占用8字节(64位系统)
- 实际的字符串对象存储在堆内存的其他位置
切片操作的内存行为:
# 切片创建新列表(浅拷贝) sub_list = stocks[1:3] # 创建新列表,包含对原对象的引用 # 修改原列表不影响切片 stocks[1] = '新券商' # sub_list[1]仍然是'海通证券'append() vs insert() 性能对比: | 操作 | 时间复杂度 | 说明 | |——|———–|——| |
append()| amortized O(1) | 在末尾添加,无需移动其他元素 | |insert(0, x)| O(n) | 在开头添加,需要移动所有元素 | |pop()| O(1) | 弹出末尾元素 | |pop(0)| O(n) | 弹出首元素,需要移动所有元素 |金融应用:实时行情更新:
# 模拟实时价格序列 prices = [] # 空列表,用于存储价格历史 # append()高效添加新价格 for new_price in [10.5, 10.6, 10.55, 10.7]: prices.append(new_price) # 计算简单移动平均 # sum()和len()都是O(1)时间复杂度 avg_price = sum(prices) / len(prices) print(f'平均价格: {avg_price:.2f}')
42.3 列表方法与时间复杂度分析
# 创建数值列表,用于演示各种列表方法
# 这个列表包含重复元素,便于演示count()方法
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
# sorted()函数:返回排序后的新列表
# 原列表保持不变,sorted()使用Timsort算法,时间复杂度O(n log n)
# Timsort是归并排序和插入排序的混合算法,针对现实数据优化
numbers_sorted = sorted(numbers)
print('排序:', numbers_sorted) # 输出: [1, 1, 2, 3, 4, 5, 6, 9]
# reverse()方法:原地反转列表
# 这是一个就地操作,不创建新列表,时间复杂度O(n)
numbers_sorted.reverse() # 反转为降序
print('反转:', numbers_sorted) # 输出: [9, 6, 5, 4, 3, 2, 1, 1]
# count()方法:统计元素出现次数
# 需要遍历整个列表,时间复杂度O(n)
print(f'计数(1): {numbers_sorted.count(1)}') # 输出: 2 (数字1出现了2次)
# sum()函数:计算列表元素和
# 这是Python内置函数,时间复杂度O(n)
print(f'求和: {sum(numbers_sorted)}') # 输出: 31 (9+6+5+4+3+2+1+1)
# len()函数:获取列表长度
# Python内部维护列表长度信息,时间复杂度O(1)
print(f'长度: {len(numbers_sorted)}') # 输出: 8
# 列表推导式(List Comprehension):创建新列表的优雅方式
# 语法: [expression for item in iterable]
# 列表推导式比传统for循环更快,因为使用了字节码优化
squares = [x**2 for x in numbers_sorted] # 计算每个元素的平方
print('平方:', squares) # 输出: [81, 36, 25, 16, 9, 4, 1, 1]易混淆概念辨析:sort() vs sorted()
| 方法 | 原地修改 | 返回值 | 使用场景 |
|---|---|---|---|
list.sort() |
是 | None | 不需要保留原列表,节省内存 |
sorted(list) |
否 | 新列表 | 需要保留原顺序 |
# sort()方法示例
original = [3, 1, 2]
original.sort() # 原地修改
print(original) # [1, 2, 3]
# original变量指向的列表已被修改
# sorted()函数示例
original = [3, 1, 2]
new_list = sorted(original) # 创建新列表
print(original) # [3, 1, 2] - 原列表不变
print(new_list) # [1, 2, 3] - 新列表排序补充说明:Timsort算法的金融应用优势
Python的Timsort算法特别适合金融时间序列数据: 1. 自适应:对部分有序数据效率极高(接近O(n)) 2. 稳定排序:相等元素的相对顺序保持不变 3. 内存优化:对临时空间的需求很小
列表操作时间复杂度完整表:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
lst[i] |
O(1) | 索引访问 |
lst.append(x) |
amortized O(1) | 末尾追加 |
lst.insert(i, x) |
O(n) | 任意位置插入 |
lst.pop() |
O(1) | 弹出末尾 |
del lst[i] |
O(n) | 删除任意位置 |
lst.remove(x) |
O(n) | 删除指定值 |
x in lst |
O(n) | 线性搜索 |
lst.index(x) |
O(n) | 查找索引 |
len(lst) |
O(1) | 获取长度 |
lst.sort() |
O(n log n) | 排序 |
42.4 金融应用投资组合管理
# 定义持仓股票组合
# 这是一个包含字典的列表,每个字典代表一只股票的持仓信息
# 列表允许我们存储多个股票,字典允许每只股票有多个属性
portfolio = [
{'code': '600519.SH', 'name': '贵州茅台', 'shares': 100, 'price': 1850.00},
{'code': '000858.SZ', 'name': '五粮液', 'shares': 200, 'price': 220.50},
{'code': '600036.SH', 'name': '招商银行', 'shares': 500, 'price': 45.20}
]
# 初始化总投资价值为0
total_value = 0
# 遍历投资组合中的每只股票
# for循环逐个访问列表中的元素(这里是字典)
for stock in portfolio:
# 计算单只股票的持仓价值
# stock['shares']获取持股数量
# stock['price']获取股票价格(注意:原代码中第二项使用了'shares'键,可能是笔误)
value = stock['shares'] * stock['price']
# 累加到总投资
# += 是增量赋值操作符,等价于 total_value = total_value + value
total_value += value
# 使用f-string格式化输出
# stock['name']获取股票名称
# :,.2f 表示格式化为带千分位的两位小数
print(f"{stock['name']}: {value:,.2f}元")
# 输出总投资
# \n 是换行符
print(f'\n总投资: {total_value:,.2f}元')代码深度解析:
列表+字典的数据结构设计:
# 这种嵌套结构非常适合表示"实体-属性"关系 # 列表提供顺序和可变性,字典提供灵活的属性访问 # 等价的面向对象设计(需要先定义类) class Stock: def __init__(self, code, name, shares, price): self.code = code self.name = name self.shares = shares self.price = price # 对于简单数据结构,列表+字典更轻量 # 对于复杂行为,面向对象设计更合适遍历模式的性能考量:
# 方式1:直接遍历元素(本例使用) for stock in portfolio: print(stock['name']) # 方式2:遍历索引 for i in range(len(portfolio)): print(portfolio[i]['name']) # 方式1更Pythonic,方式2在需要索引时使用计算投资组合权重的扩展:
# 计算每只股票的权重 weights = [] for stock in portfolio: value = stock['shares'] * stock['price'] weight = value / total_value weights.append(weight) print(f"{stock['name']}: {weight:.2%}") # 验证权重之和是否为1(或100%) print(f'权重总和: {sum(weights):.4f}') # 应该输出1.0000实际应用:持仓分析:
# 找出持仓价值最高的股票 max_value = 0 max_stock = None for stock in portfolio: value = stock['shares'] * stock['price'] if value > max_value: max_value = value max_stock = stock print(f'最大持仓: {max_stock["name"]}, 价值: {max_value:,.2f}元')
列表的内存效率分析:
对于n个元素的列表: - 引用数组: n × 8字节(64位系统) - 对象开销: 每个对象约56字节(Python对象头) - 实际数据: 取决于对象内容
例如,包含1000个整数的列表: - 引用数组: 8 KB - 整数对象: 56 KB (1000 × 56) - 总计: 约64 KB
相比之下,NumPy数组只需约8 KB(紧凑存储),这正是科学计算使用NumPy的原因。
最佳实践总结:
- 选择列表的场景:
- 需要频繁添加/删除元素
- 元素类型不同
- 需要保持插入顺序
- 数据量较小(< 10,000个元素)
- 避免列表的场景:
- 频繁在列表头部插入/删除 → 使用
collections.deque - 需要快速查找 → 使用字典或集合
- 大规模数值计算 → 使用NumPy数组
- 需要频繁拼接字符串 → 使用
str.join()
- 频繁在列表头部插入/删除 → 使用
- 性能优化技巧:
- 使用
list.append()而非list + [x](后者创建新列表) - 使用列表推导式而非for循环
- 预分配列表大小(如果已知):
[None] * n - 使用
extend()批量添加元素而非循环append()
- 使用